//
// Created by gj on 16-2-11.
//

#include "base64.h"

#include <math.h>

// 判断输入缓冲区是否读完
#define _IN_NO_EOF        (inCount < inSize)
// 判断输出缓冲区是否满
#define _OUT_NO_EOF    (outCount < outSize)
// 向输出流写入一个字符
#define _PUT_OUT(out) \
    if(_OUT_NO_EOF) { \
        output[outCount++] = (uint8_t)(out);\
    } else {\
        return BASE64_FILE_IO_ERROR; \
    }
// 判断是否填入‘=’
#define CHECK_FLAG_NO_PADDING    (flags & BASE64_FLAG_NO_PADDING)
// 是否需要76行换行
#define CHECK_FLAG_NO_WRAP        (flags & BASE64_FLAG_NO_WRAP)
// 是否为CRLF格式而不是LF格式
#define CHECK_FLAG_NO_CRLF        (flags & BASE64_FLAG_CRLF)
// 是否为url传输模式
#define CHECK_FLAG_NO_URL_SAFE    (flags & BASE64_FLAG_URL_SAFE)

/**
 * 计算encode后的长度
 */
int32_t base64_encode_buffsize(int32_t inSize, int32_t flags) {
    int32_t outSize = inSize + (inSize % 3 == 0 ? 0 : (3 - inSize % 3));
    outSize = outSize / 3 * 4;
    if (!CHECK_FLAG_NO_WRAP) {
        // 判断PADDING标记是CRLF还是LF
        int8_t pd = CHECK_FLAG_NO_PADDING ? 1 : 2;
        // 计算都多少个PADDING
        outSize = outSize + ceil((double) outSize / 76) * pd;
    }
    return outSize;
}

int32_t base64_decode_buffsize(int32_t inSize) {
    return ceil((double) inSize / 4) * 3;
}

/**
 * encodeblock
 *
 * encode 3 8-bit binary bytes as 4 '6-bit' characters
 */
static inline void base64_encodeblock(uint8_t *in, uint8_t *out, int32_t len, int32_t flags) {
    // see BASE64_FLAG_NO_PADDING
    uint8_t pd = CHECK_FLAG_NO_PADDING ? '\0' : '=';
    // see BASE64_FLAG_URL_SAFE
    const uint8_t *_c64 = CHECK_FLAG_NO_URL_SAFE ? cu64 : cb64;
    out[0] = (uint8_t) _c64[(int32_t) (in[0] >> 2)];
    out[1] = (uint8_t) _c64[(int32_t) (((in[0] & 0x03) << 4) | ((in[1] & 0xf0) >> 4))];
    out[2] = (uint8_t) (len > 1 ? _c64[(int32_t) (((in[1] & 0x0f) << 2) | ((in[2] & 0xc0) >> 6))] : pd);
    out[3] = (uint8_t) (len > 2 ? _c64[(int32_t) (in[2] & 0x3f)] : pd);
}

/**
 * encode
 *
 * base64 encode a stream adding padding and line breaks as per spec.
 * 使用文件的这个函数暂时不测时，使用前需要测试
 */
int32_t base64_encode_forfile(FILE *infile, FILE *outfile, int32_t flags) {
    uint8_t in[3];
    uint8_t out[4];
    int32_t i, len, blocksout = 0;
    int32_t retcode = 0;

    *in = (uint8_t) 0;
    *out = (uint8_t) 0;
    while (feof(infile) == 0) {
        len = 0;
        for (i = 0; i < 3; i++) {
            in[i] = (uint8_t) getc(infile);

            if (feof(infile) == 0) {
                len++;
            } else {
                in[i] = (uint8_t) 0;
            }
        }
        if (len > 0) {
            base64_encodeblock(in, out, len, flags);
            for (i = 0; i < 4; i++) {
                if (putc((int32_t) (out[i]), outfile) == 0) {
                    if (ferror(outfile) != 0) {
                        retcode = BASE64_FILE_IO_ERROR;
                    }
                    break;
                }
            }
            blocksout++;
        }
        // see BASE64_FLAG_NO_WRAP
        if (!CHECK_FLAG_NO_WRAP && (blocksout >= (BASE64_DEF_LINE_SIZE / 4) || feof(infile) != 0)) {
            if (blocksout > 0) {
                // see BASE64_FLAG_NO_CRLF
                CHECK_FLAG_NO_CRLF ? fprintf(outfile, "\r\n") : fprintf(outfile, "\n");
                blocksout = 0;
            }
        }
    }
    return (retcode);
}

int32_t base64_encode(const uint8_t *input, int32_t inSize, uint8_t *output, int32_t &outSize, int32_t flags) {
    int32_t retcode = 0;
    int32_t blocksout = 0;
    uint8_t in[3];
    uint8_t out[4];

    *in = (uint8_t) 0;
    *out = (uint8_t) 0;

    int32_t inCount = 0;
    int32_t outCount = 0;

    while (_IN_NO_EOF) {
        int32_t len = 0;
        for (int32_t i = 0; i < 3; ++i) {
            in[i] = (uint8_t) input[inCount];
            if (_IN_NO_EOF) {
                ++len;
            } else {
                in[i] = (uint8_t) 0;
            }
            ++inCount;
        }
        if (len > 0) {
            base64_encodeblock(in, out, len, flags);
            for (int i = 0; i < 4; ++i) {
                _PUT_OUT(out[i])
            }
            ++blocksout;
        }
        // see BASE64_FLAG_NO_WRAP
        if (!CHECK_FLAG_NO_WRAP && (blocksout >= (BASE64_DEF_LINE_SIZE / 4) || (!_IN_NO_EOF))) {
            if (blocksout > 0) {
                // see BASE64_FLAG_NO_URL_SAFE
                if (!CHECK_FLAG_NO_URL_SAFE) {
                    _PUT_OUT('\r');
                }
                _PUT_OUT('\n');
                blocksout = 0;
            }
        }
    }
    outSize = outCount;
    return retcode;
}

/**
 * decodeblock
 *
 * decode 4 '6-bit' characters into 3 8-bit binary bytes
 */
static inline void base64_decodeblock(uint8_t *in, uint8_t *out) {
    out[0] = (uint8_t) (in[0] << 2 | in[1] >> 4);
    out[1] = (uint8_t) (in[1] << 4 | in[2] >> 2);
    out[2] = (uint8_t) (((in[2] << 6) & 0xc0) | in[3]);
}

/**
 * 在解码的时候需要判断，如果对方用的时url编码模式需要转换回来
 */
static inline uint8_t base64_transform_urlsafe(uint8_t c) {
    if (c == '-') {
        return '+';
    }
    if (c == '_') {
        return '/';
    }
    return c;
}

/**
 * decode
 *
 * decode a base64 encoded stream discarding padding, line breaks and noise
 * 使用文件的这个函数暂时不测时，使用前需要测试
 */
int32_t base64_decode_forfile(FILE *infile, FILE *outfile) {
    int32_t retcode = 0;
    uint8_t in[4];
    uint8_t out[3];
    int32_t v;
    int32_t i, len;

    *in = (uint8_t) 0;
    *out = (uint8_t) 0;
    while (feof(infile) == 0) {
        for (len = 0, i = 0; i < 4 && feof(infile) == 0; i++) {
            v = 0;
            while (feof(infile) == 0 && v == 0) {
                v = getc(infile);
                if (v != EOF) {
                    v = base64_transform_urlsafe(v);
                    v = ((v < 43 || v > 122) ? 0 : (int32_t) cd64[v - 43]);
                    if (v != 0) {
                        v = ((v == (int32_t) '$') ? 0 : v - 61);
                    }
                }
            }
            if (feof(infile) == 0) {
                len++;
                if (v != 0) {
                    in[i] = (uint8_t) (v - 1);
                }
            } else {
                in[i] = (uint8_t) 0;
            }
        }
        if (len > 0) {
            base64_decodeblock(in, out);
            for (i = 0; i < len - 1; i++) {
                if (putc((int32_t) out[i], outfile) == 0) {
                    if (ferror(outfile) != 0) {
                        perror(base64_message(BASE64_FILE_IO_ERROR));
                        retcode = BASE64_FILE_IO_ERROR;
                    }
                    break;
                }
            }
        }
    }
    return (retcode);
}

/**
 *
 */
int32_t base64_decode(const uint8_t *input, int32_t inSize, uint8_t *output, int32_t &outSize) {
    int32_t retcode = 0;
    uint8_t in[4];
    uint8_t out[3];

    *in = (uint8_t) 0;
    *out = (uint8_t) 0;

    int32_t inCount = 0;
    int32_t outCount = 0;

    while (_IN_NO_EOF) {
        int32_t len = 0;
        for (int32_t i = 0; i < 4 && _IN_NO_EOF; ++i) {
            int32_t v = 0;
            while (_IN_NO_EOF && v == 0) {
                v = base64_transform_urlsafe(input[inCount++]);
                v = ((v < 43 || v > 122) ? 0 : (int32_t) cd64[v - 43]);
                if (v != 0) {
                    v = ((v == (int32_t) '$') ? 0 : v - 61);
                }
            }
            if (_IN_NO_EOF) {
                ++len;
                if (v != 0) {
                    in[i] = (uint8_t) (v - 1);
                }
            } else {
                in[i] = (uint8_t) 0;
            }
        }
        if (len > 0) {
            base64_decodeblock(in, out);
            for (int i = 0; i < len - 1; ++i) {
                _PUT_OUT(out[i]);
            }
        }
    }
    outSize = outCount;
    return retcode;
}

/**
 * b64
 *
 * 'engine' that opens streams and calls encode/decode
 */

static int32_t base64(uint8_t opt, uint8_t *infilename, uint8_t *outfilename, int32_t linesize) {
    FILE *infile;
    int32_t retcode = BASE64_FILE_ERROR;

    if (!infilename) {
        infile = stdin;
    } else {
        infile = fopen((char *) infilename, "rb");
    }
    if (!infile) {
        perror((char *) infilename);
    } else {
        FILE *outfile;
        if (!outfilename) {
            outfile = stdout;
        } else {
            outfile = fopen((char *) outfilename, "wb");
        }
        if (!outfile) {
            perror((char *) outfilename);
        } else {
            if (opt == 'e') {
                retcode = base64_encode_forfile(infile, outfile, linesize);
            } else {
                retcode = base64_decode_forfile(infile, outfile);
            }
            if (retcode == 0) {
                if (ferror(infile) != 0 || ferror(outfile) != 0) {
                    perror(base64_message(BASE64_FILE_IO_ERROR));
                    retcode = BASE64_FILE_IO_ERROR;
                }
            }
            if (outfile != stdout) {
                if (fclose(outfile) != 0) {
                    perror(base64_message(BASE64_ERROR_OUT_CLOSE));
                    retcode = BASE64_FILE_IO_ERROR;
                }
            }
        }
        if (infile != stdin) {
            if (fclose(infile) != 0) {
                perror(base64_message(BASE64_ERROR_OUT_CLOSE));
                retcode = BASE64_FILE_IO_ERROR;
            }
        }
    }

    return (retcode);
}

/**
 * showuse
 *
 * display usage information, help, version info
 */
static void showuse(int32_t morehelp) {
    {
        printf("\n");
        printf("  b64      (Base64 Encode/Decode)      Bob Trower 08/03/01 \n");
        printf("           (C) Copr Bob Trower 1986-01.      Version 0.12R \n");
        printf("  Usage:   b64 -option [-l<num>] [<FileIn> [<FileOut>]]\n");
        printf("  Purpose: This program is a simple utility that implements\n");
        printf("           Base64 Content-Transfer-Encoding (RFC1113).\n");
    }
    if (morehelp == 0) {
        printf("           Use -h option for additional help.\n");
    } else {
        printf("  Options: -e  encode to Base64   -h  This help text.\n");
        printf("           -d  decode from Base64 -?  This help text.\n");
        printf("  Note:    -l  use to change line size (from 72 characters)\n");
        printf("  Returns: 0 = success.  Non-zero is an error code.\n");
        printf("  ErrCode: 1 = Bad Syntax, 2 = File Open, 3 = File I/O\n");
        printf("  Example: b64 -e binfile b64file     <- Encode to b64\n");
        printf("           b64 -d b64file binfile     <- Decode from b64\n");
        printf("           b64 -e -l40 infile outfile <- Line Length of 40\n");
        printf("  Note:    Will act as a filter, but this should only be\n");
        printf("           used on text files due to translations made by\n");
        printf("           operating systems.\n");
        printf("  Source:  Source code and latest releases can be found at:\n");
        printf("           http://base64.sourceforge.net\n");
        printf("  Release: 0.12.00, Mon Feb 14 17:17:00 2011, ANSI-SOURCE C\n");
    }
}

/**
 * main
 *
 * parse and validate arguments and call b64 engine or help
 */
int32_t main(int32_t argc, char **argv) {
    uint8_t opt = (uint8_t) 0;
    int32_t retcode = 0;
    int32_t linesize = BASE64_DEF_LINE_SIZE;
    uint8_t *infilename = NULL, *outfilename = NULL;

    while (THIS_OPT(argc, argv) != (uint8_t) 0) {
        switch (THIS_OPT(argc, argv)) {
            case 'l':
                linesize = atoi(&(argv[1][2]));
                if (linesize < BASE64_MIN_LINE_SIZE) {
                    linesize = BASE64_MIN_LINE_SIZE;
                    printf("%s\n", base64_message(BASE64_LINE_SIZE_TO_MIN));
                }
                break;
            case '?':
            case 'h':
                opt = 'h';
                break;
            case 'e':
            case 'd':
                opt = THIS_OPT(argc, argv);
                break;
            default:
                opt = (uint8_t) 0;
                break;
        }
        argv++;
        argc--;
    }
    if (argc > 3) {
        printf("%s\n", base64_message(BASE64_SYNTAX_TOOMANYARGS));
        opt = (uint8_t) 0;
    }
    switch (opt) {
        case 'e':
        case 'd':
            infilename = (uint8_t *) (argc > 1 ? argv[1] : NULL);
            outfilename = (uint8_t *) (argc > 2 ? argv[2] : NULL);
            retcode = base64(opt, infilename, outfilename, linesize);
            break;
        case 0:
            if (argv[1] == NULL) {
                showuse(0);
            } else {
                retcode = BASE64_SYNTAX_ERROR;
            }
            break;
        case 'h':
            showuse((int32_t) opt);
            break;

    }
    if (retcode != 0) {
        printf("%s\n", base64_message(retcode));
    }

    return (retcode);
}